﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Data;
using System.Collections;
using System.Reflection;
using MessageServer.serialize;
using MessageServer.config;

namespace MessageServer.persistence
{
   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field,
       Inherited=true)]
    public class PersistInto : System.Attribute
    {
       String _value;

       public PersistInto(String val)
       {
           _value = val;
       }

       public string value { get { return _value; } }
    }


    class TableColumn
    {
        static Hashtable MAP_TYPES = new Hashtable();
        String _name;
        SqlDbType _type;
        int _length;
        String _default;
        bool _isPrimary;

        internal static void initializeTypes()
        {
            if (MAP_TYPES.Count <= 0)
            {
                MAP_TYPES.Add("int", SqlDbType.Int);
                MAP_TYPES.Add("varchar", SqlDbType.VarChar);
                MAP_TYPES.Add("binary", SqlDbType.VarBinary);
                MAP_TYPES.Add("datetime", SqlDbType.DateTime);
            }
        }

        private SqlDbType map(String type)
        {
            if (MAP_TYPES.ContainsKey(type))
                return (SqlDbType)MAP_TYPES[type];

            throw new Exception("Not supported type: " + type);
        }

        internal TableColumn(SqlDataReader reader)
        {
            int cnt = 0;
            _name = (String)reader[cnt]; cnt++;
            String type = (String)reader[cnt]; cnt++;
            _type = map(type);
            var smthng = reader[cnt]; cnt++;
            if ((smthng != null) && !(smthng is System.DBNull))
            {
                Type t = smthng.GetType();
                _length = (int)smthng;
            }
            else
                _length = -1;

            smthng = reader[cnt]; cnt++;
            if ((smthng != null) && !(smthng is System.DBNull))
                _default = (String)smthng;

            smthng = reader[cnt]; cnt++;
            if ((smthng != null) && !(smthng is System.DBNull))
                _isPrimary = true;
            else
                _isPrimary = false;
        }

        internal String name { get { return _name; } }
        internal SqlDbType type { get { return _type; } }
        internal int length { get { return _length; } }
        internal String defvalue { get { return _default; } }
        internal bool isPrimary { get { return _isPrimary; } }
    }

    class CacheElement
    {
        String _table;
        List<TableColumn> _cols;
        SqlCommand _insertcmd;
        SqlCommand _selectcmd;
        object lockinsert = new object();
        object lockselect = new object();

        internal CacheElement(String table, List<TableColumn> cols)
        {
            _table = table;
            _cols = cols;
        }

        internal List<TableColumn> primaryCols()
        {
            List<TableColumn> primes = new List<TableColumn>();
            foreach (TableColumn col in _cols)
            {
                if (col.isPrimary) primes.Add(col);
            }

            return primes;
        }

        internal SerializableMessage populate(SqlDataReader reader, SerializableMessage obj)
        {
            Hashtable map = obj.mapping;
            int colcnt = 0;
            foreach (TableColumn col in _cols)
            {
                FieldInfo fld = (FieldInfo)map[col.name];
                fld.SetValue(obj, reader[colcnt]); colcnt++;
            }
            return obj;
        }

        internal SqlCommand insertcmd()
        {
            if (_insertcmd == null)
            {
                lock (lockinsert)
                {
                    if (_insertcmd == null)
                    {
                        String cmd = "INSERT INTO " + _table + "(";
                        String add = "";
                        String vals = "";
                        List<SqlParameter> parms = new List<SqlParameter>();
                        foreach (TableColumn col in _cols)
                        {
                            String parmname = "@" + col.name;
                            cmd = cmd + add + col.name;
                            vals = vals + add + parmname;
                            add = ", ";
                            SqlParameter parm;
                            if (col.length > 0)
                                parm = new SqlParameter(parmname, col.type, col.length);
                            else
                                parm = new SqlParameter(parmname, col.type);

                            parms.Add(parm);
                        }
                        cmd += ") VALUES (" + vals + ")"; ;

                        _insertcmd = new SqlCommand(cmd);
                        foreach (SqlParameter parm in parms)
                        {
                            _insertcmd.Parameters.Add(parm);
                        }
                    }
                }
            }

            return _insertcmd;
        }

        internal SqlCommand selectcmd()
        {
            if (_selectcmd == null)
            {
                lock (lockselect)
                {
                    if (_selectcmd == null)
                    {
                        String cmd = "SELECT ";
                        String add = "";
                        String where = " WHERE ";
                        String WhereAdd = "";
                        List<SqlParameter> parms = new List<SqlParameter>();
                        foreach (TableColumn col in _cols)
                        {
                            cmd = cmd + add + col.name;
                            add = ", ";
                            if (col.isPrimary)
                            {
                                String parmname = "@" + col.name;
                                where = where + WhereAdd + col.name + " = " + parmname;
                                WhereAdd = " AND ";
                                SqlParameter parm;
                                if (col.length > 0)
                                    parm = new SqlParameter(parmname, col.type, col.length);
                                else
                                    parm = new SqlParameter(parmname, col.type);

                                parms.Add(parm);
                            }
                        }
                        cmd = cmd + " FROM " + _table + where;

                        _selectcmd = new SqlCommand(cmd);
                        foreach (SqlParameter parm in parms)
                            _selectcmd.Parameters.Add(parm);
                    }
                }
            }

            return _selectcmd;
        }
    }

    public class PersistentContext
    {
        MessageServerConfig _config;
        SqlConnection _connection;
        SerializeFormat _format;

        public PersistentContext(MessageServerConfig config)
        {
            TableColumn.initializeTypes();
            _config = config;
            _connection = new SqlConnection(_config.connectionString);
            Type stype = Type.GetType(_config.persistenceFormat);
            ConstructorInfo construct = stype.GetConstructor(new Type[0]);
            _format = (SerializeFormat)construct.Invoke(new object[0]);
        }

        public SqlConnection connection { get { return _connection; } }
        public SerializeFormat format { get { return _format; } }
    }

    class PersistentObject
    {
        String DESCRIBE_TABLE = "SELECT COLUMN_NAME, DATA_TYPE, " +
            " CHARACTER_MAXIMUM_LENGTH, COLUMN_DEFAULT, " +
            " (SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS b " +
            " WHERE (TABLE_NAME = a.TABLE_NAME) AND " +
            " (CONSTRAINT_NAME LIKE 'PK__%') AND (COLUMN_NAME = a.COLUMN_NAME)) " +
            " AS IS_PRIMARY " +
            " FROM         INFORMATION_SCHEMA.COLUMNS a " +
            " WHERE     (TABLE_NAME = @table)";

        static Hashtable TABLES = new Hashtable();

        PersistentContext _context;
        SqlConnection _conn;
        object cachelock = new object();

        private CacheElement cached(String table)
        {
            if (!TABLES.ContainsKey(table))
            {
                lock (cachelock)
                {
                    if (!TABLES.ContainsKey(table))
                    {
                        _conn.Open();
                        List<TableColumn> columns;
                        SqlCommand cmd = new SqlCommand(DESCRIBE_TABLE, _conn);
                        cmd.Parameters.Add(new SqlParameter("@table", SqlDbType.VarChar, 50));
                        cmd.Parameters[0].Value = table;
                        SqlDataReader reader = cmd.ExecuteReader();
                        columns = new List<TableColumn>();
                        while (reader.Read())
                        {
                            TableColumn col = new TableColumn(reader);
                            columns.Add(col);
                        }
                        TABLES.Add(table, new CacheElement(table, columns));
                        _conn.Close();
                    }
                }
            }

            return (CacheElement)TABLES[table];
        }

        internal PersistentObject(PersistentContext context)
        {
            _context = context;
            _conn = context.connection;
        }

        private int insert(SqlCommand cmd, SerializableMessage obj)
        {
            Hashtable map = obj.mapping;
            foreach (String fldname in map.Keys)
            {
                FieldInfo fld = (FieldInfo)map[fldname];
                cmd.Parameters["@" + fldname].Value = fld.GetValue(obj);
            }
            cmd.Connection.Open();
            int ret = cmd.ExecuteNonQuery();
            cmd.Connection.Close();
            return ret;
        }

        internal int create(SerializableMessage obj)
        {
            int ret = 0;
            Type t = obj.GetType();
            if (t.IsDefined(typeof(PersistInto), false))
            {
                PersistInto p = (PersistInto)Attribute.GetCustomAttribute(t, typeof(PersistInto));
                CacheElement cachetbl = cached(p.value);
                SqlCommand cmd = cachetbl.insertcmd();
                cmd.Connection = _conn;
                ret = insert(cmd, obj);
            }

            return ret;
        }

        private SerializableMessage populate(SqlCommand cmd, SerializableMessage obj, CacheElement element)
        {
            Hashtable map = obj.mapping;
            List<TableColumn> primes = element.primaryCols();
            foreach (TableColumn col in primes)
            {
                FieldInfo fld = (FieldInfo)map[col.name];
                cmd.Parameters["@" + col.name].Value = fld.GetValue(obj);
            }
            cmd.Connection.Open();
            SqlDataReader reader = cmd.ExecuteReader();
            if (reader.Read())
            {
                //read the first element, only one element shd exist for the given primary key
                element.populate(reader, obj);
            }
            cmd.Connection.Close();
            return obj;
        }

        internal SerializableMessage retrieve(SerializableMessage obj)
        {
            Type t = obj.GetType();
            if (t.IsDefined(typeof(PersistInto), false))
            {
                PersistInto p = (PersistInto)Attribute.GetCustomAttribute(t, typeof(PersistInto));
                CacheElement cachetbl = cached(p.value);
                SqlCommand cmd = cachetbl.selectcmd();
                cmd.Connection = _conn;
                obj = populate(cmd, obj, cachetbl);
            }

            return obj;
        }
    }

    /**
     * Default constructor creator to deserialize serialized and persisted objects. All
     * persisted objects should have default constructors.
     * */
    public class CreateMe : ObjectConstructor
    {
        //invoke the default constructor and return an object of type t
        public object construct(Type t)
        {
            ConstructorInfo construct = t.GetConstructor(new Type[0]);
            return construct.Invoke(new object[0]);
        }
    }

    class SerializableMessage
    {
        PersistentContext _context;
        SerializeFormat _format;
        Hashtable _mapping;                 //store flds that map to a column

        internal void storeMap()
        {
            if (_mapping == null)
            {
                _mapping = new Hashtable();
                Type t = this.GetType();
                FieldInfo[] flds = t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                foreach (FieldInfo fld in flds)
                {
                    if (fld.IsDefined(typeof(PersistInto), false))
                    {
                        PersistInto p = (PersistInto)Attribute.GetCustomAttribute(fld, typeof(PersistInto));
                        _mapping.Add(p.value, fld);
                    }
                }
            }
        }

        internal SerializableMessage(PersistentContext context)
        {
            _context = context;
            _format = context.format;
            storeMap();
        }

        internal Hashtable mapping { get { return _mapping; } }

        protected byte[] serialized(Object obj)
        {
            return _format.serialize(obj);
        }

        protected Object deserialized(byte[] bytes, String type)
        {
            int deserializedlen = 0;
            return _format.deserialize(bytes, Type.GetType(type), 
                new CreateMe(), ref deserializedlen);
        }

        public int persist()
        {
            PersistentObject p = new PersistentObject(_context);
            return p.create(this);
        }

        public SerializableMessage retrieve()
        {
            PersistentObject p = new PersistentObject(_context);
            return p.retrieve(this);
        }
    }

    [PersistInto("MESSAGE_RECEIVED")]
    class ReceivedMessage : SerializableMessage
    {
        [PersistInto("MESSAGE_ID")]
        int _messageId;

        [PersistInto("REMOTE_ADDRESS")]
        String _remoteAddress;

        [PersistInto("MESSAGE_CONTENT")]
        byte[] _content;

        [PersistInto("MESSAGE_STATUS")]
        int _status;

        [PersistInto("RECEIVED_DATETIME")]
        DateTime _time;

        [PersistInto("MESSAGE_TYPE")]
        String _type;

        Object _message;

        public ReceivedMessage(PersistentContext context, Object msg, String remoteAddress) : base(context)
        {
            _messageId = msg.GetHashCode();
            _remoteAddress = remoteAddress;
            _content = serialized(msg);
            _status = (int)messagestatus.unacknowledged;
            _message = msg;
            _time = DateTime.Now;
            _type = msg.GetType().FullName;
        }

        public ReceivedMessage(PersistentContext ctx, Object msg) : base(ctx)
        {
            _messageId = msg.GetHashCode();
            _status = (int)messagestatus.invalid;
        }

        public messagestatus status { get { return (messagestatus)_status; } set { _status = (int)value; } }
    }
    
    [PersistInto("MESSAGE_SENT")]
    class SentMessage : SerializableMessage
    {
        [PersistInto("MESSAGE_ID")]
        int _messageId;

        [PersistInto("REMOTE_ADDRESS")]
        String _remoteAddress;

        [PersistInto("MESSAGE_CONTENT")]
        byte[] _content;

        [PersistInto("SEND_STATUS")]
        int _status;

        [PersistInto("CREATED_DATETIME")]
        DateTime _created;

        [PersistInto("SENT_DATETIME")]
        DateTime _sent;

        [PersistInto("RECEIVED_MESSAGE_ID")]
        int _receivedMsgId;

        [PersistInto("MESSAGE_TYPE")]
        String _type;

        Object _sentMessage;
        Object _receivedMessage;

        public SentMessage(PersistentContext context, Object msg, Object replyfor, 
            String remoteAddress) : base(context)
        {
            _messageId = msg.GetHashCode();
            _receivedMessage = replyfor;
            _receivedMsgId = replyfor.GetHashCode();
            _remoteAddress = remoteAddress;
            _content = serialized(msg);
            _status = (int)messagestatus.unacknowledged;
            _sentMessage = msg;
            _created = DateTime.Now;
            _sent = DateTime.Now;
            _type = msg.GetType().FullName;
        }

        public messagestatus status { get { return (messagestatus)_status; } set { _status = (int)value; } }
    }
}
